package cn.dengta.context.auth;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.annotation.*;
import javax.servlet.http.*;

import cn.dengta.common.context.Context;
import cn.dengta.common.redis.FastJsonRedisSerializer;
import cn.dengta.common.redis.RedisUtil;
import cn.dengta.common.security.AES;
import cn.dengta.common.util.SpringUtil;
import cn.dengta.common.web.*;
import cn.dengta.context.model.Member;
import cn.dengta.context.web.*;
import cn.dengta.webapp.base.entity.BaseEntity;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import me.codeplayer.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;

@Slf4j
@Getter
@Setter
@SuppressWarnings("rawtypes")
public class DefaultAutoLoginHandler implements AutoLoginHandler, InitializingBean {

	/** 用户记住我 Cookie key */
	public static final String rememberMeKey = "rm_ads";
	/** 用户 Token 请求头 */
	public static final String authorization = "authorization";
	public static long expireOffsetMs = TimeUnit.DAYS.toMillis(30);
	public static long renewTokenCountDownMs = TimeUnit.DAYS.toMillis(7);

	static Duration TIMEOUT = Duration.of(10, ChronoUnit.SECONDS);

	static final ThreadLocal<Long> tokenExpireTime = new ThreadLocal<>();

	public static final String CONCURRENT_SESSION_USER_PREFIX = "concurrentSessionUser:";
	public static final String CONCURRENT_LOGIN_LOCK_PREFIX = "concurrentLoginLock:";

	String key;
	final DES des;
	final AES aes;

	@Resource
	StringRedisTemplate stringRedisTemplate;
	@Resource
	FastJsonRedisSerializer springSessionDefaultRedisSerializer;
	@DubboReference
	AutoLoginSupport autoLoginSupport;

	// 前后台实现了同一个远程自动登录接口，有可能调用会窜，导致类型转换错误，因此需要手动进行适配
	transient AutoLoginSupport empAutoLoginSupport;

	@Qualifier("empAutoLoginSupport")
	@Autowired(required = false)
	public void setEmpAutoLoginSupport(AutoLoginSupport empAutoLoginSupport) {
		this.empAutoLoginSupport = empAutoLoginSupport;
	}

	public DefaultAutoLoginHandler(String key) {
		this.key = key;
		des = new DES(key);
		aes = new AES(key);
	}

	public DefaultAutoLoginHandler() {
		this("DTColEde^PlayWLDer@1O0L18#1548o.T");
	}

	/**
	 * 实现【记住我】的自动登录的准备工作
	 */
	public String rememberMe(HttpServletRequest request, HttpServletResponse response, Member member) {
		String clientId = CookieUtil.getCookieValue(request, Context.COOKIE_CLIENT_ID);
		if (StringUtil.isEmpty(clientId)) {
			clientId = UUID.randomUUID().toString();
			CookieUtil.setCookie(response, Context.COOKIE_CLIENT_ID, clientId, (int) TimeUnit.DAYS.toSeconds(365), null, true);
		}
		return flushMemberToken(request, response, member, clientId);
	}

	@Nonnull
	String buildToken(String encodeUserId, Member member, long expireTimestamp, String clientId) {
		return Encrypter.md5(encodeUserId + '@' + member.getPassword() + ':' + expireTimestamp + ':' + key + '#' + clientId);
	}

	/**
	 * 尝试自动登录。如果无效，则返回 null
	 */
	public Member tryAutoLogin(HttpServletRequest request, HttpServletResponse response) {
		final Cookie cookie = CookieUtil.getCookie(request, rememberMeKey);
		final boolean hasCookie = cookie != null;
		String tokenVal = hasCookie ? ServletUtil.decodeURL(cookie.getValue()) : request.getHeader(authorization);
		if (StringUtil.notEmpty(tokenVal) && tokenVal.length() <= 256) {
			if (cookie == null) { // 如果 Cookie 为空
				StringBuilder sb = Logs.logRequest(request, null, ProxyRequest.getAppId(request) + "无法获取Cookie：", null, null, "Cookie");
				log.error(sb.toString());
			}
			final RequestContextImpl req = RequestContextImpl.get();

			final String tokenMd5 = Encrypter.md5(tokenVal);
			Member member = RedisUtil.loadInLockAndUnlockSilently(CONCURRENT_LOGIN_LOCK_PREFIX + tokenMd5, 10_000L, () -> {
				final String redisKey = CONCURRENT_SESSION_USER_PREFIX + tokenMd5;
				String memberJson = stringRedisTemplate.opsForValue().get(redisKey);
				Member m = null;
				if (memberJson != null) {
					try {
						m = (Member) JSON.parse(memberJson, springSessionDefaultRedisSerializer.getParserConfig(), springSessionDefaultRedisSerializer.getDeserializeFeatures());
					} catch (Exception e) {
						log.error("解析自动登录用户JSON数据时出错", e);
					}
				}
				if (m == null) {
					Boolean userOrEmp = null;
					final boolean fromBackstage = Context.get().fromBackstage(request);
					if (fromBackstage) {
						// 后台代理商使用的是前台用户进行登录
						userOrEmp = StringUtils.contains(request.getHeader(Context.internal().getAppIdHeaderName()), "_agent/PC/");
					}
					m = parseToken(userOrEmp, tokenVal, hasCookie ? TokenSource.Cookie : TokenSource.Header, CookieUtil.getCookieValue(request, Context.COOKIE_CLIENT_ID));
					if (m != null) {
						String jsonStr = JSON.toJSONString(m, springSessionDefaultRedisSerializer.getSerializeFeatures());
						stringRedisTemplate.opsForValue().setIfAbsent(redisKey, jsonStr, TIMEOUT);
						if (!autoLoginSupport.autoLoginCallback(m, req.getSessionContext(request).getClient(), fromBackstage)) {
							request.getSession().invalidate();
							SessionCookieUtil.clearCookies(request, response, "/");
							return null;
						}
						tryRenewToken(request, response, m);
					}
				}
				return m;
			});

			if (member != null && req.sessionUser() == null) {
				req.sessionUser(member);
			}
			return member;
		}
		if (cookie != null) {
			CookieUtil.removeCookie(response, cookie); // remove if invalid
		}
		return null;
	}

	@Override
	public Member parseToken(@Nullable Boolean userOrEmp, String tokenVal, TokenSource tokenSource, @Nullable String clientId) {
		String text;
		try {
			text = des.decode(tokenVal);
		} catch (Exception e) {
			log.error("解析自动登录 Token 时出错：" + tokenVal, e);
			text = null;
		}
		final String[] parts = StringUtils.split(text, ':');
		if (parts != null && parts.length == 4) {
			String userID = parts[0];
			Long userId;
			try {
				userId = BaseEntity.alias2Id(userID);
			} catch (Exception e) {
				log.error("解析自动登录的 userId 时出错：" + userID, e);
				userId = null;
			}
			final long expireTime = NumberUtil.getLong(parts[1], -1);
			if (tokenSource == TokenSource.WebSocket || expireTime >= System.currentTimeMillis()) {
				if (userId != null) {
					Member user = userOrEmp != null ? autoLoginSupport.load(userOrEmp, userId) : autoLoginSupport.load(userId);
					if (user != null && user.isValid()) {
						boolean match = true;
						if (StringUtil.notEmpty(clientId)) {
							String token = parts[2];
							String expectToken = buildToken(userID, user, expireTime, clientId);
							match = expectToken.equals(token);
						}
						if (match) {
							tokenExpireTime.set(expireTime);
							return user;
						}
					}
				}
			} else {
				final HttpServletRequest request = RequestContextImpl.get().getRequest();
				log.warn("用户{} 的Token已过期：{} => {}", userId, tokenVal, request == null ? "" : request.getRequestURI());
			}
		}
		return null;
	}

	@Override
	public String parseSessionId(String urlToken) {
		String text;
		try {
			text = des.decode(urlToken.replace(" ", "+"));
		} catch (Exception e) {
			log.error("解析自动登录 Token 时出错：" + urlToken, e);
			text = null;
		}
		final String[] parts = StringUtils.split(text, ':');
		return parts != null && parts.length >= 4 ? parts[3] : null;
	}

	@Override
	public int updateSessionId(String sessionId, Long userId) {
		return autoLoginSupport.updateSessionId(sessionId, userId);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (empAutoLoginSupport != null) {
			autoLoginSupport = empAutoLoginSupport;
		} else if (isAdmin(autoLoginSupport)) {
			log.warn("检测到 autoLoginSupport 注入异常={}", autoLoginSupport);
			autoLoginSupport = SpringUtil.getBeansOfType(AutoLoginSupport.class).values().stream().filter(t -> !isAdmin(t)).findFirst().orElseThrow();
		}
	}

	static boolean isAdmin(Object obj) {
		return obj.getClass().getName().contains(".admin.");
	}

	@Nullable
	public String tryRenewToken(HttpServletRequest request, HttpServletResponse response, Member member) {
		final Long expireTimeInMs = tokenExpireTime.get();
		if (expireTimeInMs != null && (expireTimeInMs - System.currentTimeMillis()) < renewTokenCountDownMs) {
			try {
				String clientId = CookieUtil.getCookieValue(request, Context.COOKIE_CLIENT_ID);
				return flushMemberToken(request, response, member, clientId);
			} catch (Exception e) {
				log.error("自动登录Token续期失败：" + member.getId(), e);
			}
		}
		return null;
	}

	protected String flushMemberToken(HttpServletRequest request, HttpServletResponse response, Member member, String clientId) {
		final long expireTime = System.currentTimeMillis() + expireOffsetMs;
		final String userID = BaseEntity.id2Alias(member.getId());
		String token = buildToken(userID, member, expireTime, clientId);
		String cookieValueSource = userID + ':' + expireTime + ':' + token + ':' + request.getSession().getId();
		String cookieValue = des.encode(cookieValueSource);
		CookieUtil.setCookie(response, rememberMeKey, cookieValue, (int) (expireOffsetMs / 1000), null, true);
		return cookieValue;
	}

}
